Skip to content

Ultrasound cine#878

Open
PaulHax wants to merge 6 commits into
Kitware:mainfrom
PaulHax:ultrasound-cine
Open

Ultrasound cine#878
PaulHax wants to merge 6 commits into
Kitware:mainfrom
PaulHax:ultrasound-cine

Conversation

@PaulHax
Copy link
Copy Markdown
Collaborator

@PaulHax PaulHax commented May 14, 2026

No description provided.

PaulHax added 6 commits May 14, 2026 11:40
Lands a self-contained cine pipeline for single-file ultrasound DICOMs
(SOP Class UID 1.2.840.10008.5.1.4.1.1.3 / .3.1, NumberOfFrames > 1)
alongside the existing volume pipeline. Multi-chunk volume imports never
match the cine router, so CT/MR streaming and 3D volume behavior are
unchanged.

Core additions under src/core/cine/:
- parseCineDicom.ts wraps cornerstonejs/dicom-parser to extract the
  header (transfer syntax, geometry, FrameTime, patient/study/series,
  SequenceOfUltrasoundRegions) and per-frame byte views — zero-copy for
  native PixelData, fragment-aware for encapsulated JPEG-Baseline (with
  populated BOT, empty BOT, and JPEG-SOI scan fallbacks). Supports
  Implicit + Explicit VR LE and JPEG-Baseline.
- DicomCineImage extends BaseProgressiveImage, owns one 2D vtkImageData
  (extent [0,cols-1, 0,rows-1, 0,0], 3-component RGB uint8), and swaps
  scalars in-place when the selected frame changes. setFrame() bumps a
  decode token unconditionally so any new request — cached or decode —
  invalidates in-flight decodes.
- frameCache.ts: byte-budgeted LRU keyed by frame index; decodeJpegFrame
  via createImageBitmap + OffscreenCanvas; decodeNativeFrame for native
  RGB / MONOCHROME2.
- isCineImage / getCineImage helpers and getRenderSlice that returns 0
  for cine so the VTK mapper renders slice 0 while the semantic slice is
  the frame index.

Import routing in src/store/datasets-dicom.ts: when a chunk group has a
single chunk, an UltrasoundMultiframe SOP UID (current or retired), and
NumberOfFrames > 1, it diverts to _importCineChunk before the legacy
DicomChunkImage path. View integration:
- VtkBaseSliceRepresentation.vue: render-slice helper pins VTK slice to
  0 for cine; conditional W/L sync (cine pixels are display-encoded, so
  wlConfig defaults don't clobber them); slice watch is immediate so
  restored sessions paint the saved frame on first mount.
- SliceViewerOverlay shows "Frame: N/M" and hosts a new CineTransport
  component (play/pause/loop/fps via useIntervalFn, FPS seeded from
  FrameTime).
- Ruler/Rectangle/Polygon widgets use getRenderSlice for their plane
  manipulator origin so annotations still scope to a frame.
- view-configs/slicing.ts overrides the slice range to
  [0, numberOfFrames - 1] for cine.
- image-cache.removeImage now calls dispose() before delete; cine's
  dispose clears the LRU, drops compressed frame refs, and deletes the
  vtkImageData.
- image-stats early-returns for cine ids — histogram/auto-range is
  meaningless on display-encoded data.

Testing:
- 3 vitest tests build a synthetic DICOM in-memory (Explicit VR LE
  native, encapsulated with populated BOT, encapsulated with empty BOT)
  to exercise the parser without external fixtures.
- New tests/specs/cine-rendering.e2e.ts loads US-MONO2-8-8x-execho.dcm
  from the BSD-licensed GDCM corpus on SourceForge (cached via the
  existing wdio onPrepare hook into .tmp/), asserts the cine transport
  renders with "1 / 8", and asserts the counter advances on ArrowUp.

Adds dicom-parser ^1.8.21 (MIT, 0 deps, ~6.9 KB gzipped) to
devDependencies.
…meInfo.kind

Promote getThumbnail(strategy) to the ProgressiveImage interface with a
default null implementation on BaseProgressiveImage. Cine images and
LoadedVtkImage inherit the null thumbnail automatically, so the data
browser falls back to modality text instead of spinning forever when a
cine DICOM is selected.

Replace every `instanceof DicomChunkImage|DicomCineImage` check with a
read of useDICOMStore().volumeInfo[id]?.kind === 'cine':

- isCineImage / getCineImage now branch on the store tag.
- datasets-dicom guards both the cine bail and the chunk-volume reuse
  against the same kind.
- segmentGroups skips the SEG-decoding branch for cine ids so a cine
  image can't reach chunkImage.getModality() and crash.
- PatientStudyVolumeBrowser just calls image.getThumbnail() — the
  workaround added for cine in the previous commit is gone.

ThumbnailStrategy moves to progressiveImage.ts; chunkImage.ts re-exports
it for back-compat. DicomChunkImage.getThumbnail return type tightens
from Promise<any> to Promise<string | null>.
Each 2D view now renders cine from its own local vtkImageData, so two
views can hold different frames or play independently without overwriting
each other's pixels. The canonical cine vtkImageData stays as a
compatibility surface for metadata and older consumers.

- DicomCineImage exposes getFrame(n) backed by FrameCache plus an
  inFlightFrames map so concurrent views share a single decode.
- VtkBaseSliceRepresentation builds a per-component CineRenderImage
  (vtkImageData + RGB scalars), binds the mapper to it for cine, and
  copies decoded frames into it with stale-token guards.
- startLoad() now seeds the canonical scalar buffer via getFrame(0); the
  public setFrame/currentFrame/getCurrentFrame/decodeToken API is removed.
- Scalar probe is unmounted and cleared for cine since it samples the
  canonical (frame-0) image.
getThumbnail() only ever supported MiddleSlice, so callers always passed
the same value and DicomChunkImage threw on anything else. Remove the
parameter and the enum.
The slice manipulators set the active view from a watcher on a ref that
is bidirectionally synced with sliceConfig.slice. Cine playback writes
to that ref every frame, so two playing views fought over which was
active and the green selection ring flickered.

Fire setActiveView only from the manipulator's user-input callback so
the active view changes on real wheel/drag input, not on programmatic
writes that come back through syncRef.
Cine images report dimensions [cols, rows, 1], so tools placed on any
frame past index 0 had their frame-of-reference resolution fail the
bounds check and jumpToTool returned early. Pass allowOutOfBoundsSlice
so we still get the axis and can drive the view's slice config.
@netlify
Copy link
Copy Markdown

netlify Bot commented May 14, 2026

Deploy Preview for volview-dev ready!

Name Link
🔨 Latest commit 47d2dc7
🔍 Latest deploy log https://app.netlify.com/projects/volview-dev/deploys/6a05ed2918dbd700080000e1
😎 Deploy Preview https://deploy-preview-878--volview-dev.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant